package com.lipaluma.parser.post.treatment.unmarshall.merge;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang.StringUtils;

import com.lipaluma.annotations.merge.CustomMerge;
import com.lipaluma.annotations.merge.Id;
import com.lipaluma.annotations.merge.Merge;
import com.lipaluma.annotations.merge.MergeTarget;
import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.field.mapper.FieldId;
import com.lipaluma.field.mapper.FieldIdAssignable;
import com.lipaluma.field.mapper.FieldMapper;
import com.lipaluma.field.mapper.FieldMergeable;
import com.lipaluma.field.mapper.single.impl.ObjectFieldMapper;
import com.lipaluma.field.mapper.single.impl.OneToManyFieldMapper;
import com.lipaluma.field.merge.FieldMergedAlgorithm;
import com.lipaluma.field.merge.FieldMergedAlgorithmEnum;
import com.lipaluma.field.merge.FieldMergedFunction;
import com.lipaluma.field.merge.MergingTargetFunction;
import com.lipaluma.field.merge.NoMergeAlgorithm;
import com.lipaluma.field.merge.TargetMergedAlgorithm;
import com.lipaluma.parser.post.treatment.unmarshall.AllLinesTreatmentProcessor;
import com.lipaluma.util.ReflectionUtils;
import com.lipaluma.xml.XmlMappings;
import com.lipaluma.xml.mapping.XmlFieldMapping;
import com.lipaluma.xml.mapping.XmlObjectFieldMapping;

public class MergeLinesProcessor<TARGET> implements AllLinesTreatmentProcessor<TARGET> {
	
	private Map<Class<?>, List<FieldId>> fieldsIdByClass = new HashMap<Class<?>, List<FieldId>>();
	private Map<Class<?>, List<Field>> oneToManyFieldsByClass = new HashMap<Class<?>, List<Field>>();
	private Map<Class<?>, Map<Field, FieldMergedFunction>> fieldsMergedByClass = new HashMap<Class<?>, Map<Field,FieldMergedFunction>>();
	
	private Map<Class<?>, Map<String, Object>> objectsRegroupedById = new HashMap<Class<?>, Map<String,Object>>();
	private Map<Class<?>, List<Object>> objectsGroupedByEquals = new HashMap<Class<?>, List<Object>>();
	private Set<Class<?>> mergeableClasses = new HashSet<Class<?>>();
	private Map<Class<?>, MergingTargetFunction> classesMergedBy = new HashMap<Class<?>, MergingTargetFunction>();
	private Set<Class<?>> classesParsed = new HashSet<Class<?>>();
	
	public MergeLinesProcessor() {}
	public MergeLinesProcessor(Class<TARGET> clazz, boolean targetMergeable) {
		initNewTargetClass(clazz);
		if(targetMergeable){
			initTargetClassMergeable(clazz);
		}
	}
	public MergeLinesProcessor(Class<TARGET> clazz, MergingTargetFunction mergingTargetFunction) {
		initNewTargetClass(clazz);
		if(mergingTargetFunction != null){
			initTargetClassMergeable(clazz, mergingTargetFunction);
		}
	}

	public boolean init(List<FieldMapper> fieldMappers) {
		if(fieldMappers == null || fieldMappers.isEmpty())
			throw new CsvParserException("Field mappers can't be null or empty during the initialization of the "+MergeLinesProcessor.class);
		Class<?> targetClass = fieldMappers.get(0).getField().getDeclaringClass();
		if(!classesParsed.contains(targetClass)) {
			initNewTargetClass(targetClass);
			MergeTarget mergeTarget = targetClass.getAnnotation(MergeTarget.class);
			if(mergeTarget != null) {
				if(!NoMergeAlgorithm.class.equals(mergeTarget.value())) {
					TargetMergedAlgorithm algorithm = instanceNewAlgorithm(mergeTarget.value());
					MergingTargetFunction function = new MergingTargetFunction(algorithm, mergeTarget.params());
					initTargetClassMergeable(targetClass, function);
				} else 
					initTargetClassMergeable(targetClass);
				
			}
		}
		processInitFieldMappersRecursively(fieldMappers);
		return !mergeableClasses.isEmpty();
	}

	public boolean init(List<FieldMapper> fieldMappers, XmlMappings mappings) {
		if(fieldMappers == null || fieldMappers.isEmpty())
			throw new CsvParserException("Field mappers can't be null or empty during the initialization of the "+MergeLinesProcessor.class);
		Class<?> targetClass = fieldMappers.get(0).getField().getDeclaringClass();
		if(!classesParsed.contains(targetClass)) {
			initNewTargetClass(targetClass);
			if(mappings.isTargetCanBeMerged()) {
				if(mappings.targetMergedBy().getAlgorithm() != null) {
					MergingTargetFunction function = new MergingTargetFunction(mappings.targetMergedBy().getAlgorithm(), mappings.targetMergedBy().getParams());
					initTargetClassMergeable(targetClass, function);
				} else 
					initTargetClassMergeable(targetClass);
			}
		}
		Map<Field, XmlFieldMapping> mappingByField = getMappingByField(fieldMappers, mappings.getMappings());
		processInitFieldMappersRecursively(fieldMappers, mappingByField);
		return !mergeableClasses.isEmpty();
	}

	private void processInitFieldMappersRecursively(List<FieldMapper> fieldMappers) {
		for (FieldMapper fieldMapper : fieldMappers) {
			if(isFieldId(fieldMapper)) {
				List<FieldId> listFieldsId = fieldsIdByClass.get(fieldMapper.getField().getDeclaringClass());
				listFieldsId.add((FieldId)fieldMapper);
			}
			FieldMergedFunction fieldMergedFunction = checkMergeableFieldMapper(fieldMapper);
			if(fieldMergedFunction != null) {
				Map<Field, FieldMergedFunction> fieldsToMerge = fieldsMergedByClass.get(fieldMapper.getField().getDeclaringClass());
				fieldsToMerge.put(fieldMapper.getField(), fieldMergedFunction);
			}
			if(OneToManyFieldMapper.class.isAssignableFrom(fieldMapper.getClass())) {
				initOneToMany((OneToManyFieldMapper)fieldMapper);
				fieldMapper = ((OneToManyFieldMapper)fieldMapper).getFieldMapper();
			}
			if(ObjectFieldMapper.class.isAssignableFrom(fieldMapper.getClass())) {
				ObjectFieldMapper objectFieldMapper = (ObjectFieldMapper)fieldMapper;
				initNewTargetClass(objectFieldMapper.getTargetClass());
				
				if(isTargetMerged(objectFieldMapper)) {
					MergingTargetFunction function = getMergingTargetInfos(objectFieldMapper);
					if(function != null)
						initTargetClassMergeable(objectFieldMapper.getTargetClass(), function);
					else
						initTargetClassMergeable(objectFieldMapper.getTargetClass());
				}
				processInitFieldMappersRecursively(objectFieldMapper.getFieldMappers());
			}
		}
	}

	private void processInitFieldMappersRecursively(List<FieldMapper> fieldMappers, Map<Field, ? extends XmlFieldMapping> mappings) {
		for (FieldMapper fieldMapper : fieldMappers) {
			XmlFieldMapping xmlMapping = mappings.get(fieldMapper.getField());
			if(isFieldId(fieldMapper, xmlMapping)){
				List<FieldId> listFieldsId = fieldsIdByClass.get(fieldMapper.getField().getDeclaringClass());
				listFieldsId.add((FieldId)fieldMapper);
			}
			FieldMergedFunction fieldMergedFunction = checkMergeableFieldMapper(fieldMapper, xmlMapping);
			if(fieldMergedFunction != null) {
				Map<Field, FieldMergedFunction> fieldsToMerge = fieldsMergedByClass.get(fieldMapper.getField().getDeclaringClass());
				fieldsToMerge.put(fieldMapper.getField(), fieldMergedFunction);
			}
			if(OneToManyFieldMapper.class.isAssignableFrom(fieldMapper.getClass())) {
				initOneToMany((OneToManyFieldMapper)fieldMapper);
				fieldMapper = ((OneToManyFieldMapper)fieldMapper).getFieldMapper();
			}
			if(ObjectFieldMapper.class.isAssignableFrom(fieldMapper.getClass())) {
				ObjectFieldMapper objectFieldMapper = (ObjectFieldMapper)fieldMapper;
				XmlObjectFieldMapping xmlObjectMapping = (XmlObjectFieldMapping)xmlMapping;
				initNewTargetClass(objectFieldMapper.getTargetClass());
				if(xmlObjectMapping.isTargetCanBeMerged()) {
					if(xmlObjectMapping.targetMergedBy().getAlgorithm() != null) {
						MergingTargetFunction function = new MergingTargetFunction(xmlObjectMapping.targetMergedBy().getAlgorithm(), xmlObjectMapping.targetMergedBy().getParams());
						initTargetClassMergeable(objectFieldMapper.getTargetClass(), function);
					} else
						initTargetClassMergeable(objectFieldMapper.getTargetClass());
				}
				Map<Field, XmlFieldMapping> newMappings = getMappingByField(objectFieldMapper.getFieldMappers(), xmlObjectMapping.getMappings());
				processInitFieldMappersRecursively(objectFieldMapper.getFieldMappers(), newMappings);
			}
		}
	}

	@Override
	public List<TARGET> execute(List<TARGET> targets) {
		List<TARGET> result = new ArrayList<TARGET>();
		for (TARGET target : targets) {
			try {
				processMergingRecursively(target);
				result.add(target);
			} catch (ObjectMergedException e) {}
		}
		return result;
	}

	private void processMergingRecursively(Object target) throws ObjectMergedException {
		List<Field> fieldsAlreadyMerged = new ArrayList<Field>();
		for (Field field : target.getClass().getDeclaredFields()) {
			List<Field> fieldsInOneToMany = oneToManyFieldsByClass.get(target.getClass());
			if(fieldsInOneToMany != null && fieldsInOneToMany.contains(field)) {
				Class<?> parameterizedClass = ReflectionUtils.getParameterizedClass(field);
				if(classesParsed.contains(parameterizedClass)) {
					try {
						ReflectionUtils.makeAccessible(field);
						Collection<Object> collection = (Collection<Object>)ReflectionUtils.getField(field, target);
						if(collection != null && !collection.isEmpty())
							processMergingRecursively(collection.iterator().next());
					} catch (ObjectMergedException e) {
						fieldsAlreadyMerged.add(field);
					}
				}
			} else if(classesParsed.contains(field.getType())) {
				try {
					ReflectionUtils.makeAccessible(field);
					processMergingRecursively(ReflectionUtils.getField(field, target));
				} catch (ObjectMergedException e) {
					ReflectionUtils.setField(field, target, e.getObjectMerged());
					fieldsAlreadyMerged.add(field);
				}
			}
		}
		if(!mergeableClasses.contains(target.getClass()))
			return;
		HashMap<Field, FieldMergedFunction> fieldsToMerge = new HashMap<Field, FieldMergedFunction>();
		fieldsToMerge.putAll(fieldsMergedByClass.get(target.getClass()));
		for(Field field : fieldsAlreadyMerged)
			fieldsToMerge.remove(field);
		
		List<FieldId> listFieldsId = fieldsIdByClass.get(target.getClass());
		if(listFieldsId != null && !listFieldsId.isEmpty()) {
			mergeTargetById(target, listFieldsId, fieldsToMerge);
		} else {
			mergeTargetByEquals(target, fieldsToMerge);
		}
	}
	
	private void mergeTargetById(Object target, List<FieldId> fieldsId, Map<Field, FieldMergedFunction> fieldsToMerge) throws ObjectMergedException {
		String key = generateKey(target, fieldsId);
		if(StringUtils.isEmpty(key)) {
//						LOG.warn("An empty id was generated on a target ("+targetClass+") to be merged by id or the line "+);
		}
		Map<String, Object> mapObjectsById = objectsRegroupedById.get(target.getClass());
		if(mapObjectsById == null) {
			mapObjectsById = new HashMap<String, Object>();
			mapObjectsById.put(key, target);
			objectsRegroupedById.put(target.getClass(), mapObjectsById);

			MergingTargetFunction function = classesMergedBy.get(target.getClass());
			if(function != null) {
				function.getAlgorithm().initMergeOnFirstElement(target, function.getParams());
			}
		}
		else if(mapObjectsById.get(key) == null) {
			mapObjectsById.put(key, target);
		} else {
			Object realTarget = mapObjectsById.get(key);
			mergeFields(realTarget, target, fieldsToMerge);

			MergingTargetFunction function = classesMergedBy.get(realTarget.getClass());
			if(function != null) {
				Object result = mergeTarget(realTarget, target, function);
				mapObjectsById.put(key, result);
			}
			throw new ObjectMergedException(realTarget);
		}
	}

	private void mergeTargetByEquals(Object target, Map<Field, FieldMergedFunction> fieldsToMerge) throws ObjectMergedException {
		List<Object> objectsGrouped = objectsGroupedByEquals.get(target.getClass());
		if(objectsGrouped == null) {
			objectsGrouped = new ArrayList<Object>();
			objectsGrouped.add(target);
			objectsGroupedByEquals.put(target.getClass(), objectsGrouped);

			MergingTargetFunction function = classesMergedBy.get(target.getClass());
			if(function != null) {
				function.getAlgorithm().initMergeOnFirstElement(target, function.getParams());
			}
			return;
		}
		int index = objectsGrouped.indexOf(target);
		if(index == -1) {
			objectsGrouped.add(target);
		} else {
			Object realTarget = objectsGrouped.get(index);
			mergeFields(realTarget, target, fieldsToMerge);

			MergingTargetFunction function = classesMergedBy.get(realTarget.getClass());
			if(function != null) {
				objectsGrouped.remove(realTarget);
				Object result = mergeTarget(realTarget, target, function);
				objectsGrouped.add(result);
			}
			
			throw new ObjectMergedException(realTarget);
		}
	}

	private String generateKey(Object target, List<FieldId> fieldsId) {
		StringBuilder key = new StringBuilder();
		for (FieldId fieldId : fieldsId) {
			key.append(fieldId.generateKey(target));
		}
		return key.toString();
	}	

	private void mergeFields(Object realTarget, Object addon, Map<Field, FieldMergedFunction> fieldsToMerge) {
		for (Entry<Field, FieldMergedFunction> entry : fieldsToMerge.entrySet()) {
			FieldMergedFunction fieldMergedFunction = entry.getValue();
			if(fieldMergedFunction != null) {
				FieldMergedAlgorithm mergeByValue = (FieldMergedAlgorithm) fieldMergedFunction.getAlgorithm();
				Object oldValue = ReflectionUtils.getField(entry.getKey(), realTarget);
				Object newValue = ReflectionUtils.getField(entry.getKey(), addon);
				Object mergedValue = mergeByValue.merge(oldValue, newValue, fieldMergedFunction.getParams());
				ReflectionUtils.setField(entry.getKey(), realTarget, mergedValue);
			}
		}
	}

	private Object mergeTarget(Object realTarget, Object addon, MergingTargetFunction function) {
		return function.getAlgorithm().merge(realTarget, addon, function.getParams());
	}

	public void initNewTargetClass(Class<?> targetClass) {
		classesParsed.add(targetClass);
		oneToManyFieldsByClass.put(targetClass, new ArrayList<Field>());
		fieldsIdByClass.put(targetClass, new ArrayList<FieldId>());
		fieldsMergedByClass.put(targetClass, new HashMap<Field, FieldMergedFunction>());
	}
	
	public void initTargetClassMergeable(Class<?> targetClass) {
		mergeableClasses.add(targetClass);
	}
	public void initTargetClassMergeable(Class<?> targetClass, MergingTargetFunction function) {
		mergeableClasses.add(targetClass);
		classesMergedBy.put(targetClass, function);
	}

	private void initOneToMany(OneToManyFieldMapper fieldMapper) {
		List<Field> oneToManyFields = oneToManyFieldsByClass.get(fieldMapper.getField().getDeclaringClass());		
		oneToManyFields.add(fieldMapper.getField());
	}

	public FieldMergedFunction checkMergeableFieldMapper(FieldMapper fieldMapper) {
		Merge merge = fieldMapper.getField().getAnnotation(Merge.class);
		CustomMerge customMerge = fieldMapper.getField().getAnnotation(CustomMerge.class);
		if(merge != null && customMerge != null)
			throw new CsvParserException("Error on field "+fieldMapper.getField().getName()+" : Only one annotation is authorized to declare the merging.");
		if(merge != null) {
			FieldMergedAlgorithmEnum algorithm = merge.value();
			return new FieldMergedFunction(algorithm, merge.params());
		} else if (customMerge != null){
			try {
				FieldMergedAlgorithm algorithm = customMerge.value().newInstance();
				return new FieldMergedFunction(algorithm, customMerge.params());
			} catch (InstantiationException e) {
				throw new CsvParserException("Error on field "+fieldMapper.getField().getName()+" : The merging "+customMerge.value()+" can't be Instantiated", e);
			} catch (IllegalAccessException e) {
				throw new CsvParserException("Error on field "+fieldMapper.getField().getName()+" : The merging "+customMerge.value()+" can't be Instantiated", e);
			}
		} else if(OneToManyFieldMapper.class.isAssignableFrom(fieldMapper.getClass())) {
			return new FieldMergedFunction(FieldMergedAlgorithmEnum.ADD_ALL);
		}
		else if(FieldMergeable.class.isAssignableFrom(fieldMapper.getClass())) {
			FieldMergeable fieldMergeable = (FieldMergeable)fieldMapper;
			if(fieldMergeable.hasToBeMerged())
				return fieldMergeable.mergedBy();
		}
		return null;
	}

	public FieldMergedFunction checkMergeableFieldMapper(FieldMapper fieldMapper, XmlFieldMapping mapping) {
		if(mapping.fieldMerged())
			return mapping.mergedBy();
		else if (mapping.isOneToMany())
			return new FieldMergedFunction(FieldMergedAlgorithmEnum.ADD_ALL);
		return null;
	}

	private boolean isFieldId(FieldMapper fieldMapper) {
		if(FieldId.class.isAssignableFrom(fieldMapper.getClass()) && fieldMapper.getField().isAnnotationPresent(Id.class)) {
			return true;
		}
		if(FieldIdAssignable.class.isAssignableFrom(fieldMapper.getClass())) {
			return ((FieldIdAssignable)fieldMapper).isIdAssigned();
		}
		return false;
	}
	
	private boolean isFieldId(FieldMapper fieldMapper, XmlFieldMapping mapping) {
		return FieldId.class.isAssignableFrom(fieldMapper.getClass()) && mapping.isId();
	}
		
	private Map<Field, XmlFieldMapping> getMappingByField(List<FieldMapper> fieldMappers, List<? extends XmlFieldMapping> mappings) {
		Map<Field, XmlFieldMapping> mapResult = new HashMap<Field, XmlFieldMapping>();
		for (FieldMapper fieldMapper : fieldMappers) {
			for (XmlFieldMapping xmlFieldMapping : mappings) {
				if(fieldMapper.getField().getName().equals(xmlFieldMapping.getField())){
					mapResult.put(fieldMapper.getField(), xmlFieldMapping);
					break;
				}
			}
		}
		return mapResult;
	}

	private boolean isTargetMerged(ObjectFieldMapper fieldMapper) {
		if(fieldMapper.getTargetClass().isAnnotationPresent(MergeTarget.class)) {
			return true;
		} else {
			return fieldMapper.isTargetCanBeMerged();
		}
	}
	
	private MergingTargetFunction getMergingTargetInfos(ObjectFieldMapper fieldMapper) {
		if(fieldMapper.getTargetClass().isAnnotationPresent(MergeTarget.class)) {
			MergeTarget annotation = fieldMapper.getTargetClass().getAnnotation(MergeTarget.class);
			TargetMergedAlgorithm algorithm = instanceNewAlgorithm(annotation.value());
			return new MergingTargetFunction(algorithm, annotation.params());
		} else {
			return fieldMapper.getMergingTargetInfos();
		}
	}
	
	private TargetMergedAlgorithm instanceNewAlgorithm(Class<? extends TargetMergedAlgorithm> algorithm) {
		try {
			return algorithm.newInstance();
		} catch (InstantiationException e) {
			throw new CsvParserException("Error during instanciation of the Class "+algorithm, e);
		} catch (IllegalAccessException e) {
			throw new CsvParserException("Error during instanciation of the Class "+algorithm, e);
		}
	}
}